package com.google.code.facebookapi;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONException;
import org.json.JSONObject;
import org.mortbay.jetty.Request;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.AbstractHandler;

import com.Ostermiller.util.Browser;

public class FacebookSessionTestUtils {

	protected static Log log = LogFactory.getLog( FacebookSessionTestUtils.class );

	public static final String LOGIN_BASE_URL = "https://www.facebook.com/login.php";
	public static final String PERM_BASE_URL = "http://www.facebook.com/connect/prompt_permissions.php";

	public static JUnitProperties junitProperties;

	public static JUnitProperties getJunitProperties() {
		if ( junitProperties == null ) {
			junitProperties = new JUnitProperties();
		}
		return junitProperties;
	}

	public static void clearSessions() throws IOException {
		getJunitProperties().clearSessions();
	}

	public static JSONObject attainSession() throws Exception {
		JSONObject out = getJunitProperties().loadSession();
		if ( out == null ) {
			out = attainSessionRaw2();
			junitProperties.storeSession( out );
		}
		return out;
	}

	public static <T extends IFacebookRestClient> T getSessionlessValidClient( Class<T> clientReturnType ) {
		return getSessionlessIFacebookRestClient( clientReturnType );
	}

	public static <T extends IFacebookRestClient> T getValidClient( Class<T> clientReturnType ) {
		try {
			JSONObject session_info = attainSession();
			return createRestClient( clientReturnType, session_info );
		}
		catch ( Exception ex ) {
			throw BasicClientHelper.runtimeException( ex );
		}
	}

	private static <T extends IFacebookRestClient> T createRestClient( Class<T> clientReturnType, JSONObject session_info ) throws JSONException {
		String apikey = session_info.getString( "api_key" );
		String secret = session_info.getString( "secret" );
		String sessionkey = session_info.getString( "session_key" );
		return createRestClient( clientReturnType, apikey, secret, sessionkey );
	}

	private static <T extends IFacebookRestClient> T createRestClient( Class<T> clientReturnType, String apikey, String secret, String sessionkey ) {
		try {
			Constructor<T> clientConstructor = clientReturnType.getConstructor( String.class, String.class, String.class );
			return clientConstructor.newInstance( apikey, secret, sessionkey );
		}
		catch ( Exception ex ) {
			throw new RuntimeException( "Couldn't create relevant IFacebookRestClient using reflection", ex );
		}
	}

	private static <T extends IFacebookRestClient> T getSessionlessIFacebookRestClient( Class<T> clientReturnType ) {
		try {
			Constructor<T> clientConstructor = clientReturnType.getConstructor( String.class, String.class );
			return clientConstructor.newInstance( getJunitProperties().getAPIKEY(), getJunitProperties().getSECRET() );
		}
		catch ( Exception ex ) {
			throw new RuntimeException( "Couldn't create relevant IFacebookRestClient using reflection", ex );
		}
	}

	public static <T extends IFacebookRestClient> void requirePerm( Permission perm, T client ) throws FacebookException {
		if ( !client.users_hasAppPermission( perm ) ) {
			// create http client
			HttpClient http = new HttpClient();
			http.setParams( new HttpClientParams() );
			http.setState( new HttpState() );

			// 'open' login popup/window
			Map<String,String> params = new HashMap<String,String>();
			params.put( "api_key", client.getApiKey() );
			params.put( "v", "1.0" );
			params.put( "fbconnect", "true" );
			params.put( "extern", "1" );
			params.put( "ext_perm", perm.getName() );
			params.put( "next", "http://www.facebook.com/connect/login_success.html?xxRESULTTOKENxx" );
			String queryString = BasicClientHelper.delimit( params.entrySet(), "&", "=", true ).toString();
			String url = PERM_BASE_URL + "?" + queryString;
			throw new IllegalStateException( "Require extended permission " + perm.getName() + "; please visit: " + url );
		}
	}

	public static JSONObject attainSessionRaw2() throws Exception {
		return attainSessionRaw2( getJunitProperties().getAPIKEY(), getJunitProperties().getSECRET(), 8080 );
	}

	/**
	 * http://wiki.developers.facebook.com/index.php/Authorization_and_Authentication_for_Desktop_Applications
	 */
	public static JSONObject attainSessionRaw2( String apikey, String secret, int port ) throws Exception {
		Server server = null;
		Semaphore semaphore = new Semaphore( 1 );
		semaphore.drainPermits();
		CaptureSessionHandler handler = new CaptureSessionHandler( apikey, secret, semaphore );
		try {
			{
				// start jetty to capture return value from connect login
				server = new Server( port );
				server.setHandler( handler );
				log.info( "Starting Jetty" );
				server.start();
			}
			{
				// send user to connect_login popup
				Map<String,String> params = new HashMap<String,String>();
				params.put( "api_key", apikey );
				params.put( "fbconnect", "true" );
				params.put( "v", "1.0" );
				// params.put( "connect_display", "popup" );
				params.put( "return_session", "true" );
				params.put( "req_perms", "publish_stream,read_stream,offline_access" );
				params.put( "next", "http://localhost:" + port + "/next" );
				params.put( "cancel_url", "http://localhost:" + port + "/cancel" );
				String query = BasicClientHelper.delimit( params.entrySet(), "&", "=", true ).toString();
				String url = LOGIN_BASE_URL + "?" + query;
				log.info( "Sending user to attain session:\n" + url );
				Browser.init();
				Browser.displayURL( url );
			}
			{
				// wait until jetty handler receives reply and captures session
				for ( int i = 1; i <= 12; i++ ) {
					log.debug( "Waiting for 5 sec (" + i + ")" );
					if ( semaphore.tryAcquire( 5, TimeUnit.SECONDS ) ) {
						Thread.sleep( 1000 );
						break;
					}
				}
			}
		}
		finally {
			if ( server != null && ! ( server.isStopped() || server.isStopping() ) ) {
				log.info( "Stopping Jetty" );
				server.stop();
			}
		}
		return handler.getOut();
	}



	public static class CaptureSessionHandler extends AbstractHandler {

		private JSONObject out;
		private String apikey;
		private String secret;
		private Semaphore semaphore;

		public CaptureSessionHandler( String apikey, String secret, Semaphore semaphore ) {
			this.apikey = apikey;
			this.secret = secret;
			this.semaphore = semaphore;
		}

		public JSONObject getOut() {
			return out;
		}

		@SuppressWarnings("unchecked")
		public void handle( String target, HttpServletRequest request, HttpServletResponse response, int dispatch ) throws IOException, ServletException {
			log.debug( "handle(): " + request.getRequestURL() + printMap( request.getParameterMap() ) );
			try {
				if ( target.equals( "/next" ) ) {
					out = captureSession( request, apikey, secret );
				}
				if ( target.equals( "/cancel" ) ) {
					log.warn( "User cancelled" );
					semaphore.release();
				}
				if ( target.equals( "/done" ) ) {
					log.warn( "User is done" );
					semaphore.release();
				}
			}
			catch ( JSONException ex ) {
				throw new ServletException( ex );
			}
			response.setContentType( "text/html" );
			response.setStatus( HttpServletResponse.SC_OK );
			response.getWriter().println( "<html><body><a href='/done'>done?</a></body></html>" );
			( (Request) request ).setHandled( true );
		}

		public static JSONObject captureSession( HttpServletRequest request, String apikey, String secret ) throws JSONException {
			String str = request.getParameter( "session" );
			JSONObject out = new JSONObject( str );
			out.remove( "sig" );
			out.put( "ss", out.getString( "secret" ) );
			out.put( "secret", secret );
			out.put( "api_key", apikey );
			log.debug( "session: " + out );
			return out;
		}

		public static String printMap( Map<String,String[]> map ) {
			StringBuilder sb = new StringBuilder();
			for ( Entry<String,String[]> entry : map.entrySet() ) {
				sb.append( "\n" );
				sb.append( entry.getKey() + " : " + Arrays.asList( entry.getValue() ) );
			}
			return sb.toString();
		}

	}

	public static void pauseForStreamRate() {
		log.debug( "pauseForStreamRate()" );
		try {
			Thread.sleep( 15000 );
		}
		catch ( InterruptedException ex ) {
			throw BasicClientHelper.runtimeException( ex );
		}
	}

}
